package photosalvage.entity;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;

import javax.imageio.ImageIO;
import javax.inject.Inject;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.validation.ValidationException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

import photosalvage.PhotoSalvageConstants;
import photosalvage.util.ImageUtils;
import photosalvage.util.JPAUtils;

import com.google.inject.persist.Transactional;

public class PhotoRepository extends BaseRepository<Photo, Long> {
	private static final int THUMBNAIL_S_MAX_HEIGHT = 120;
	private static final int THUMBNAIL_M_MAX_HEIGHT = 240;
	private static final int THUMBNAIL_L_MAX_HEIGHT = 600;
	
	private static final String ROOT_DIR_PATH = FileUtils.getUserDirectoryPath() + "/photosalvage/photos";
	private static final File ROOT_DIR = new File(ROOT_DIR_PATH);
	static {
		try {
			FileUtils.forceMkdir(ROOT_DIR);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
	
	@Inject
	AlbumRepository albumRepository;
	
	@Inject
	UserRepository userRepository;
	
	@Transactional
	public int delete(List<Long> photoIds) {
		Query q = getEntityManager().createQuery("delete from Photo where id in :ids");
		q.setParameter("ids", photoIds);
		return q.executeUpdate();
	}
	
	@Transactional
	public PagedList<Photo> filter(PhotoFilter filter) {
		List<String> where = new ArrayList<String>();
		Map<String, Object> params = new HashMap<String, Object>();
		if (filter.placeId != null) {
			where.add("p.album.owner.place.id = :placeId");
			params.put("placeId", filter.placeId);
		}
		if (filter.ownerId != null) {
			where.add("p.album.owner.id = :ownerId");
			params.put("ownerId", filter.ownerId);
		}
		if (filter.albumId != null) {
			where.add("p.album.id = :albumId");
			params.put("albumId", filter.albumId);
		}
		if (filter.retoucherId != null) {
			where.add("p.retouchedBy.id = :retoucherId");
			params.put("retoucherId", filter.retoucherId);
		}
		if (filter.status != null) {
			where.add("p.status = :status");
			params.put("status", filter.status);
		}
		String whereClause = "";
		if (!where.isEmpty()) {
			whereClause = " where " + StringUtils.join(where, " and ");
		}
		// まず、全体の件数を取得する
		TypedQuery<Long> countQuery =
			getEntityManager().createQuery(
					"select count(p) from Photo p" + whereClause,
					Long.class);
		for (Map.Entry<String, Object> e : params.entrySet()) {
			countQuery.setParameter(e.getKey(), e.getValue());
		}
		Long totalResults = countQuery.getSingleResult();
		
		//ソート
		String orderClause = " order by uploadedAt ";
		if (1 ==  (filter.order)) {
			orderClause = orderClause + " desc";
		}
		
		// 次に、写真データ自体を取得する
		TypedQuery<Photo> photoQuery =
			getEntityManager().createQuery(
					"select p from Photo p" + whereClause + orderClause,
					Photo.class);
		for (Map.Entry<String, Object> e : params.entrySet()) {
			photoQuery.setParameter(e.getKey(), e.getValue());
		}
		photoQuery.setFirstResult(filter.startIndex);
		photoQuery.setMaxResults(filter.itemsPerPage);
		
		List<Photo> photos = photoQuery.getResultList();
		PagedListImpl<Photo> results = new PagedListImpl<Photo>(photos);
		results.setStartIndex(filter.startIndex);
		results.setItemsPerPage(filter.itemsPerPage);
		results.setTotalResults(totalResults.intValue());
		
		return results;
	}
	@Transactional
	public Photo saveUploadedPhoto(String fileName, InputStream data, Long userId) throws IOException {
		PhotoFileInfo pi = new PhotoFileInfo(fileName);
		// '_n.jpg'でなくてはダメ
		if (!pi.ended.equals("n")) {
			throw new ValidationException("ファイル名は「_n.jpg」で終わる必要があります");
		}
		savePhoto(pi, data);
		
		Date now = new Date();
		Photo photo = findByNumber(pi.placeNum, pi.ownerNum, pi.albumNum, pi.photoNum);
		if (photo == null) {
			photo = new Photo();
		}
		photo.setNumber(pi.photoNum);
		Album album = albumRepository.findOrCreateByNumber(pi.placeNum, pi.ownerNum, pi.albumNum);
		photo.setAlbum(album);

		photo.setStatus(PhotoStatus.ORIGINAL);
		photo.setUploadedAt(now);
		photo.setUploadedBy(userRepository.find(userId));

		PhotoData original = new PhotoData();
		original.setFileName(fileName);
		original.setLastModified(now);
		photo.setOriginal(original);

		persist(photo);
		try {
			getEntityManager().flush();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return photo;
	}
	private void savePhoto(PhotoFileInfo pi, InputStream data) throws IOException {
		// 親ディレクトリの作成
		FileUtils.forceMkdir(pi.parentDir);
		
		// ファイルの保存
		FileUtils.copyInputStreamToFile(data, pi.file);
		savePhotoThumbnail(pi);
	}
	private void savePhotoThumbnail(PhotoFileInfo pi) throws IOException {
		// サムネイルの作成
		BufferedImage image = ImageIO.read(pi.file);
		String imageType = pi.extension;
		savePhotoThumbnailSub(image, imageType, THUMBNAIL_S_MAX_HEIGHT, pi.thumbnailFileS);
		savePhotoThumbnailSub(image, imageType, THUMBNAIL_M_MAX_HEIGHT, pi.thumbnailFileM);
		savePhotoThumbnailSub(image, imageType, THUMBNAIL_L_MAX_HEIGHT, pi.thumbnailFileL);
	}
	private void savePhotoThumbnailSub(BufferedImage image, String type, int maxHeight, File file) throws IOException {
		int originalWidth = image.getWidth();
		int originalHeight = image.getHeight();
		int height = Math.min(originalHeight, maxHeight);
		int width = (int) Math.floor(originalWidth * ((double) height / originalHeight));
		image = ImageUtils.scale(image, width, height);
		ImageIO.write(image, type, file);
	}
	public InputStream loadPhotoData(String fileName, PhotoSize size) {
		PhotoFileInfo pi = new PhotoFileInfo(fileName);
			try {
				File photoFile;
				switch (size) {
				case THUMBNAIL_S:
					photoFile = pi.thumbnailFileS;
					break;
				case THUMBNAIL_M:
					photoFile = pi.thumbnailFileM;
					break;
				case THUMBNAIL_L:
					photoFile = pi.thumbnailFileL;
					break;
				default:
					photoFile = pi.file;
					break;
				}
				return new FileInputStream(photoFile);
			} catch (FileNotFoundException e) {
				return null;
			}
	}
	
	@Transactional
	public Photo findByNumber(String placeNum, String ownerNum, String albumNum, String photoNum) {
		TypedQuery<Photo> q =
			getEntityManager().createQuery(
					"select p from Photo p" +
					" where p.album.owner.place.number = :placeNum" +
					"   and p.album.owner.number = :ownerNum" +
					"   and p.album.number = :albumNum" +
					"   and p.number = :photoNum", Photo.class);
		q.setParameter("placeNum", placeNum);
		q.setParameter("ownerNum", ownerNum);
		q.setParameter("albumNum", albumNum);
		q.setParameter("photoNum", photoNum);
		return JPAUtils.getSingleOrNull(q);
	}
	
	private static class PhotoFileInfo {
		@SuppressWarnings("unused")
		final String placeNum, ownerNum, albumNum, photoNum, ended, extension, fileName;
		final File parentDir, file, thumbnailDir, thumbnailFileS, thumbnailFileM, thumbnailFileL;
		
		PhotoFileInfo(String fileName) {
			this.fileName = fileName;
			Matcher m = PhotoSalvageConstants.PHOTO_FILE_NAME_PATTERN.matcher(fileName);
			if (!m.matches()) {
				throw new ValidationException(fileName + "は有効なファイル名ではありません");
			}
			this.placeNum = m.group(1);
			this.ownerNum = m.group(2);
			this.albumNum = m.group(3);
			this.photoNum = m.group(4);
			this.ended = m.group(5);
			this.extension = m.group(6);
			
			String parentDirPath = ROOT_DIR_PATH + File.separator + ownerNum + File.separator + albumNum + File.separator;
			this.parentDir = new File(parentDirPath);
			String baseFileName = photoNum + "_" + ended;
			this.file = new File(parentDir, baseFileName + "." + extension);
			
			thumbnailDir = new File(parentDir, baseFileName);
			thumbnailDir.mkdirs();
			
			thumbnailFileS = new File(thumbnailDir, PhotoSize.THUMBNAIL_S.getSizeName() + "." + extension);
			thumbnailFileM = new File(thumbnailDir, PhotoSize.THUMBNAIL_M.getSizeName() + "." + extension);
			thumbnailFileL = new File(thumbnailDir, PhotoSize.THUMBNAIL_L.getSizeName() + "." + extension);
		}
	}
	@Transactional
	public Photo endRetouch(Long photoId, String fileName, User user, InputStream data) throws IOException {
		Photo photo = find(photoId);
		if (photo == null) {
			throw new IllegalArgumentException();
		}
		User retoucher = photo.getRetouchedBy();
		if (!user.getId().equals(retoucher.getId())) {
			throw new IllegalStateException(
					String.format("レタッチャー(ID:[%s],Email:[%s])以外のユーザ(ID:[%s],Email:[%s])がレタッチを完了しようとしています。", retoucher.getId(), retoucher.getEmail(), user.getId(), user.getEmail()));
		}
		// オリジナルのファイル名_e.jpgでなければエラーとする
		String originalFileName = photo.getOriginal().getFileName();
		String expectedFileName = originalFileName.substring(0, originalFileName.lastIndexOf('_')) + "_e.jpg";
		if (!fileName.equals(expectedFileName)) {
			throw new ValidationException("期待したファイル名[" + expectedFileName + "]と異なるため、処理を中断しました");
		}
		PhotoFileInfo pi = new PhotoFileInfo(fileName);
		savePhoto(pi, data);

		Date now = new Date();
		photo.setRetouchFinishedAt(now);
		photo.setStatus(PhotoStatus.RETOUCHED);
		
		PhotoData retouched = new PhotoData();
		retouched.setFileName(fileName);
		retouched.setLastModified(now);
		photo.setRetouched(retouched);
		
		merge(photo);
		return photo;
	}
	@Transactional
	public Photo startRetouch(Long photoId, User retoucher) {
		Photo photo = find(photoId);
		photo.setStatus(PhotoStatus.RETOUCHING);
		photo.setRetouchedBy(retoucher);
		photo.setRetouchStartedAt(new Date());
		merge(photo);
		return photo;
	}
}
